从一道题说起
最近又有人问我下面这道题目,题目是这样的,首先是一个DOM
结构如下:
<html>
<head></head>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
</body>
</html>
非常easy的dom结构,在来一小段js,如下:
var nodes = document.getElementsByTagName('div');
for(var i = 0,len = nodes.length; i < len; i++){
nodes[i].onclick = function(){
console.log(i);
}
}
好了,问题来了,依次点击div,结果是多少?答案并不是1,2,3,4,5,而是点击任何一个div都会输出5.
分析
先来说下为什么最后执行的结果都是5.首先我们要明白,js中没有块级作用域,讲人话,就是js中不存在{}
这种代码块的东西。各位估计会反驳我说,上面例子中不是明明白白的写的for(){}
这种代码,怎么这边就开始说js不存在{}
这种东西呢?我先举个C++的例子吧
int arr[] = {1,2,3,4,5};
vector<int> v = vector<int>(arr,arr+sizeof(arr)/sizeof(int));
for(int i = 0; i < v.size(); i++){
std::cout << i << std::endl;
}
这么写是没有问题的,下面我再加点东西
int arr[] = {1,2,3,4,5};
vector<int> v = vector<int>(arr,arr+sizeof(arr)/sizeof(int));
for(int i = 0; i < v.size(); i++){
std::cout << i << std::endl;
}
std::cout << i;
这么写,编译器直接就报错了。提示 error: use of undeclared identifier 'i'
,很显然,出了for循环的{}的大括号对应的作用域之后,i就会被自动销毁。那么JS呢,也是这样么?我们来看个例子
for(var i = 0;i< 5;i++){
console.log(i);
}
console.log(i);
这段代码执行结果是0,1,2,3,4,5.估计有人也会比较奇怪。这边我解释下JS执行这段代码的过程。
首先是变量提升,js把var i = 0;
分解成两句话,var i;i =0;
并且把var i;提到最近一个function的顶部
,这个时候,这段代码就变成了这样
var i;
for(i=0;i<5;i++){
console.log(i);
}
console.log(i);
这样各位对于上面执行出来的0,1,2,3,4,5估计就没啥疑问了。
看完这个例子之后,我也希望各位注意下我前面说的js没有块级作用域,以及js会做变量提升,把变量的申明提升到最近的一个function的顶部
由于js会做变量提升,自动将变量的申明提升到最近的一个function的顶部,所以{}
根据不会构成所谓的块级作用域,对js里面的变量而言,只有function才会是其作用域。
好了,讲完js的变量提升,我们再回头来看最开始的这个问题。首先是变量提升,提升之后我们得到
var nodes = document.getElementsByTagName('div');
var i;
for(i = 0,len = nodes.length; i < len; i++){
nodes[i].onclick = function(){
console.log(i);
}
}
执行过程中,我们对每个node[i]节点都绑定了一个onclick事件,但是for循环执行的过程中,我们并没有出发这个click事件,for循环执行结束之后,i变为5。当用户点击div的时候,这个时候执行对应的onclick函数,也就是console.log(i),这个时候,会自动找到被js变量提升过的i,所以大家都会输出5.
解决
总结下,上面的问题之所以会产生,就是因为所有的onclick事件都去引用被js变量提升的i,那么如果我们想要解决这个问题,应该怎么办呢。一个就是我们可以通过JS的IIFE(immediately-invoked-function-expression)来构造一个作用域,让onclick函数引用我们构造出来作用域里面的i。ok,我们来解决下
var nodes = document.getElementsByTagName('div');
for(var i = 0,len = nodes.length; i < len; i++){
(function(i){
nodes[i].onclick = function(){
console.log(i);
}
})(i)
}
这种做法把整个绑定事件的过程都给包起来了,由于IIFE会马上执行,for循环的i相当于一个输入参数,在绑定完事件只有,也形成了一个作用域,并且这个作用域中存在一个i的值。
同样的道理,我再给一种解法,如下:
var nodes = document.getElementsByTagName('div');
for(var i = 0,len = nodes.length; i < len; i++){
nodes[i].onclick = (function(i){
return function(){
console.log(i);
}
})(i)
}
除此之外,我们可能会想到,如果js能够有这种块级作用于就好了,我们绑定的事件一定是在{}
作用域下面,一定可以引用到for循环中的每个i,而不是应用哪个被变量提升的i。ES6提出了用let关键字来代替var关键字,具体的话可以参考阮一峰的而ES6教程。上个代码,这边代码用了一个inbrowser的es6转码器,可以测试用,如果想要生产环境中使用需要提前将es6代码编译成es5的代码。
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script type="text/babel">
let nodes = document.getElementsByTagName('div');
console.log('exec');
for(let i = 0,len = nodes.length; i < len; i++){
nodes[i].onclick = function(){
console.log(i);
}
}
</script>>
</body>
</html>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
引用了一个inbrower级别的es6转码器。具体可以参考babel-standalone项目.改进后的代码与原来的代码的区别在于,将var i = 0
换成了let i = 0
.
下面我在看下,通过转码之后,到底生成了什么样的js代码,通过es6转码器,我们最终生成了如下的代码
var nodes = document.getElementsByTagName('div');
var _loop = function _loop(i, len) {
nodes[i].onclick = function () {
console.log(i);
};
};
for (var i = 0, len = nodes.length; i < len; i++) {
_loop(i, len);
}
原来ES6帮我们构造了一个function的作用域报过了node[i].onclick的事件绑定过程,跟我们上面的解决方法其实是一样的!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。